// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/upbit/order_v1/parser.h"

#include <utility>

#include "coin/proto/coin_query.pb.h"
#include "coin2/exchange/base/market/enums.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/util/json_util.h"

using namespace coin::proto;

namespace coin2::exchange::upbit::order_v1 {

const MarketExchangeApi UpbitParser::UPBIT_KR = MarketExchangeApi{Spot, Upbit, "v1"};
const MarketExchangeApi UpbitParser::UPBIT_ID = MarketExchangeApi{Spot, Upbitid, "v1"};
const MarketExchangeApi UpbitParser::UPBIT_SG = MarketExchangeApi{Spot, Upbitsg, "v1"};
const MarketExchangeApi UpbitParser::UPBIT_TH = MarketExchangeApi{Spot, Upbitth, "v1"};

int64_t UpbitParser::GetTimestamp(std::string_view time_str) const {
  // 2020-08-27T10:57:00+09:00 -> 2019-11-02T10:12:21Z
  std::string tmp{time_str.substr(0, 19)};
  tmp.append("Z");
  return TimestampFromIso8601(std::move(tmp));
}

bool UpbitParser::IsUpbitErrMessage(const JsonValue& data) const { return data.HasMember("error"); }

int64_t UpbitParser::GetProcOrderId(const JsonValue& data) const { return 0; }

auto UpbitParser::GetOrderSide(std::string_view side) const -> OrderSide {
  if (side == "bid") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "ask") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

auto UpbitParser::GetOrderType(std::string_view type) const -> OrderType {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market" || type == "price") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void UpbitParser::SetOrderStatus(const JsonValue& data, ProductOrderElement* product_order_element)
    const {
  // TODO(daniel) - to confirm
  std::string_view state = data["state"].GetString();
  if (state == "wait") {
    product_order_element->set_is_live(true);
  } else if (state == "done") {
    product_order_element->set_fully_filled(true);
  } else if (state == "cancel") {
    product_order_element->set_cancel_confirmed(true);
  } else {
    LOG(ERROR) << "[ParseRestOrderInfo] Unknown order status! " << state;
  }
}

void UpbitParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[UpbitSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[UpbitSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[UpbitSubmitOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() != 201) {
    LOG(ERROR) << "[UpbitSubmitOrder] failed [" << order_context->proc_order_id() << "], "
               << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    if (document.IsObject() && document.HasMember("error") && document["error"].IsObject()) {
      const auto& error = document["error"];
      if (error.HasMember("message") && error["message"].IsString()) {
        resp->set_exchange_error_msg(error["message"].GetString());
      }
      if (error.HasMember("name") && error["name"].IsString()) {
        resp->set_exchange_error_code(error["name"].GetString());
      }
    }
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_exchange_order_id(document["uuid"].GetString());
  order_mgr_->ProcessOpenOrder(document);
}

void UpbitParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    LOG(ERROR) << "[UpbitCancelOrder] AsioHttpContextStatus Error";
    return;
  }
  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[UpbitCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[UpbitCancelOrder] Parsing Error." << msg_->DebugString();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }
  if (http_context->res.result_int() != 200) {
    LOG(ERROR) << "[UpbitCancelOrder] failed [" << order_context->proc_order_id() << "], "
               << http_context->res;
    if (document.IsObject() && document.HasMember("error") && document["error"].IsObject()) {
      const auto& error = document["error"];
      if (error.HasMember("message") && error["message"].IsString()) {
        resp->set_exchange_error_msg(error["message"].GetString());
      }
      if (error.HasMember("name") && error["name"].IsString()) {
        std::string error_name = error["name"].GetString();
        if ("order_not_found" == error_name) {
          resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NOT_FOUND);
        }
        resp->set_exchange_error_code(std::move(error_name));
      }
    }
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  if (document.HasMember("state")) {
    std::string state = document["state"].GetString();
    if ("cancel" == state) {
      resp->set_cancel_confirmed(true);
      order_mgr_->DelOpenOrder(resp->exchange_order_id());
    }
  }
}

void UpbitParser::ParseRestOrderInfo(
    const JsonValue& data,
    ProductOrderElement* product_order_element) {
  /*
    {
      "uuid": "9ca023a5-851b-4fec-9f0a-48cd83c2eaae",
      "side": "ask",
      "ord_type": "limit",
      "price": "4280000.0",
      "state": "done",
      "market": "SGD-BTC",
      "created_at": "2019-01-04T13:48:09+09:00",
      "volume": "1.0",
      "remaining_volume": "0.0",
      "reserved_fee": "0.0",
      "remaining_fee": "0.0",
      "paid_fee": "2140.0",
      "locked": "0.0",
      "executed_volume": "1.0",
      "trades_count": 1,
    }
  */

  product_order_element->set_market_type(MarketType::Spot);
  product_order_element->set_exchange_type(ExchangeType::Upbit);
  product_order_element->set_api_version("v1");
  product_order_element->set_exchange_order_id(data["uuid"].GetString());
  product_order_element->set_symbol(data["market"].GetString());
  product_order_element->set_price(stod(data["price"].GetString()));
  product_order_element->set_qty(stod(data["volume"].GetString()));
  product_order_element->set_proc_order_id(GetProcOrderId(data));
  product_order_element->set_side(GetOrderSide(data["side"].GetString()));
  product_order_element->set_order_type(GetOrderType(data["ord_type"].GetString()));
  product_order_element->set_timestamp(msg_->timestamp);
  SetOrderStatus(data, product_order_element);
}

void UpbitParser::ParseRestOpenOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto pob = result_.mutable_order_list();
  pob->Clear();

  if (document.IsObject() && IsUpbitErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] failed. " << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoList] failed, unexpected order list format. "
               << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  bool product_order_bundle_found = false;
  const auto& symbols = native_symbols();
  for (const auto& info : document.GetArray()) {
    const auto& symbol = info["market"].GetString();
    if (symbols.count(symbol) == 0) {
      continue;
    }
    auto order = pob->add_each_order();
    ParseRestOrderInfo(info, order);
    product_order_bundle_found = true;
    order_mgr_->ProcessOpenOrder(info);
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

// handle rest response of 3 states
//   - wait   -- 1. update order status 2. keep order id
//   - done   -- 1. update order status 2. update fill status 3. erase order id
//   - cancel -- 1. update order status 2. update fill status 3. erase order id
void UpbitParser::ParseRestAllOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAllOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto pob = result_.mutable_order_list();
  auto pfb = result_.mutable_fill_list();
  pob->Clear();
  pfb->Clear();

  if (document.IsObject() && IsUpbitErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOrderInfoList] failed. " << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoList] failed, unexpected order list format. "
               << JsonToString(document);
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  bool product_order_bundle_found = false;
  bool product_fill_bundle_found = false;
  const auto& symbols = native_symbols();
  for (const auto& info : document.GetArray()) {
    const auto& symbol = info["market"].GetString();
    if (symbols.count(symbol) == 0) {
      continue;
    }
    auto order = pob->add_each_order();
    ParseRestOrderInfo(info, order);
    product_order_bundle_found = true;
    order_mgr_->ProcessOpenOrder(info);

    if (ParseRestFillInfoV2(info, pfb)) {
      product_fill_bundle_found = true;
    }
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void UpbitParser::ParseRestFillOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  ParseRestOrderInfoSingle(document);
  ParseRestFillInfoList(document);
}

void UpbitParser::ParseRestOrderInfoSingle(const JsonDocument& document) {
  auto pob = result_.mutable_order_list();
  pob->Clear();

  if (document.IsObject() && IsUpbitErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOrderInfoSingle] failed. " << JsonToString(document);
    return;
  }

  auto order = pob->add_each_order();
  ParseRestOrderInfo(document, order);
  result_.set_type(MsgType::ORDER_STATUS);
}

void UpbitParser::ParseRestFillInfoList(const JsonDocument& document) {
  auto pfb = result_.mutable_fill_list();
  pfb->Clear();

  if (document.IsObject() && IsUpbitErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed. " << JsonToString(document);
    return;
  }
  if (ParseRestFillInfo(document, pfb)) {
    result_.set_type(MsgType::ORDER_FILL);
  }
  order_mgr_->ProcessClosedOrder(document);
}

bool UpbitParser::ParseRestFillInfo(const JsonValue& data, ProductFillBundle* product_fill_bundle) {
  /*
    {
      "uuid": "54fbb320-5595-4655-8dd2-4924a2abf8cf",
      "side": "ask",
      "ord_type": "limit",
      "price": "9155.0",
      "state": "done",
      "market": "KRW-ATOM",
      "created_at": "2020-08-27T04:55:15+09:00",
      "volume": "5.0",
      "remaining_volume": "0.0",
      "reserved_fee": "0.0",
      "remaining_fee": "0.0",
      "paid_fee": "22.8875",
      "locked": "0.0",
      "executed_volume": "5.0",
      "trades_count": 2,
      "trades": [
        {
          "market": "KRW-ATOM",
          "uuid": "aec90f4a-196c-4e31-a4d6-eb433c7b01df",
          "price": "9155.0",
          "volume": "4.4667",
          "funds": "40892.6385",
          "created_at": "2020-08-27T04:56:24+09:00",
          "side": "ask"
        },
        {
          "market": "KRW-ATOM",
          "uuid": "0cdaae4c-405a-4164-8d32-e37d306f2e64",
          "price": "9155.0",
          "volume": "0.5333",
          "funds": "4882.3615",
          "created_at": "2020-08-27T04:56:24+09:00",
          "side": "ask"
        }
      ]
    }
  */

  const int trades_count = std::stoi(data["trades_count"].GetString());
  if (trades_count == 0) {
    return false;
  }
  if (!data.HasMember("trades")) {
    LOG(ERROR) << "[ParseRestFillInfo] failed. trade count > 0, but no trades";
    return false;
  }
  bool product_fill_bundle_found = false;
  for (const auto& info : data["trades"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["market"].GetString());
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(data["uuid"].GetString());
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(stod(info["volume"].GetString()));
    fill->set_fill_id(info["uuid"].GetString());
    product_fill_bundle_found = true;
  }
  return product_fill_bundle_found;
}

bool UpbitParser::ParseRestFillInfoV2(
    const JsonValue& data,
    ProductFillBundle* product_fill_bundle) {
  /*
    {
      "uuid": "54fbb320-5595-4655-8dd2-4924a2abf8cf",
      "side": "ask",
      "ord_type": "limit",
      "price": "9155.0",
      "state": "done",
      "market": "KRW-ATOM",
      "created_at": "2020-08-27T04:55:15+09:00",
      "volume": "5.0",
      "remaining_volume": "0.0",
      "reserved_fee": "0.0",
      "remaining_fee": "0.0",
      "paid_fee": "22.8875",
      "locked": "0.0",
      "executed_volume": "5.0",
      "trades_count": 2,
    }
  */

  std::string_view state = data["state"].GetString();
  if (state == "wait") {
    return false;
  }
  const int trades_count = std::stoi(data["trades_count"].GetString());
  if (trades_count == 0) {
    return false;
  }
  const auto& order_id = data["uuid"].GetString();
  if (!order_mgr_->Find(order_id)) {
    return false;
  }
  auto fill = product_fill_bundle->add_each_fill();
  fill->set_symbol(data["market"].GetString());
  fill->set_fill_timestamp(msg_->timestamp);
  fill->set_exchange_order_id(order_id);
  fill->set_price(stod(data["price"].GetString()));
  fill->set_qty(stod(data["executed_volume"].GetString()));
  fill->set_fill_id(order_id + std::to_string(trades_count));
  order_mgr_->ProcessClosedOrder(data);
  return true;
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto UpbitParser::ParseBalance(const JsonValue& document) -> AccountBalance {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(document, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  if (symcache_->mea() == UPBIT_KR) {
    acc_balance.set_exchange("Upbit");
  } else if (symcache_->mea() == UPBIT_ID) {
    acc_balance.set_exchange("Upbitid");
  } else if (symcache_->mea() == UPBIT_TH) {
    acc_balance.set_exchange("Upbitth");
  } else if (symcache_->mea() == UPBIT_SG) {
    acc_balance.set_exchange("Upbitsg");
  } else {
    NOTREACHED() << "unsupported mea. " << symcache_->mea().String();
  }
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }

  is_account_balance_initialized_ = true;

  return acc_balance;
}

bool UpbitParser::ParseRawBalance(const JsonValue& document, RawBalanceMap* raw_balance_map) {
  /*
  [
    {
      "currency":"SGD",
      "balance":"1000000.0",
      "locked":"0.0",
      "avg_buy_price":"0",
      "avg_buy_price_modified":false,
      "unit_currency": "SGD",
    },
    {
      "currency":"BTC",
      "balance":"2.0",
      "locked":"0.0",
      "avg_buy_price":"101000",
      "avg_buy_price_modified":false,
      "unit_currency": "SGD",
    }
  ]
  */
  if (document.IsObject() && IsUpbitErrMessage(document)) {
    LOG(ERROR) << "[ParseBalance] failed. " << JsonToString(document);
    return false;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseBalance] failed, unexpected data format. " << JsonToString(document);
    return false;
  }

  for (const auto& balance : document.GetArray()) {
    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(balance["currency"].GetString());
    double avail = stod(balance["balance"].GetString());
    double hold = stod(balance["locked"].GetString());
    currency_balance.set_available(avail);
    currency_balance.set_total(avail + hold);
    currency_balance.set_hold(hold);
    raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
  }

  return true;
}

void UpbitParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

void UpbitParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  // websocket unsupported
}

auto UpbitParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseRestAllOrders();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrders();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillOrders();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::upbit::order_v1
